/**
 * aasinchu.js
 * Created: 01/10/2013
 * Source link: http://code.google.com/p/code-depository/source/browse/
 *
 * By Suresh Nalluri,
 * License: The MIT License (MIT)
 *          See LICENSE details
 *
 * To do:
 *		- swap the ['Expected', ....].join(' ') to a function definition - done
 *		- check for cyclic loops in sfy and equals !!!
 *		- reqular expression equivalence testing (in eql)
 *		- sfy - need to sort out stringing a string !! and possible number literals - done
 *		- need to sort out withArgs & whose/test constructs
 *
 *
 *----------------------------------
 *
 *  Assertion structure (or 'string') consists of a series of left connected directives.
 *  An example of this in code would like:
 *
 *      a) expect(objA).to.be.sameAs(objB);
 *      b) expect(objA).not.to.have.property('p');
 *      c) expect(objA).length())to.equal(4.8);
 *      d) expect(14.8).not.to.equal(objA.length());
 *
 *
 * In example (a),  the leftObj of 'to' is objA and the leftDirective is null.
 *                  the leftObj of 'be' is objA and the leftDirective is 'to'.
 *
 * Assertion call paths BNF form:
 *  
 *     <Expect>         -> EXPECT(obj)[.WITHARGS][ <branch> ]
 *     <branch>         -> <to> | <not>
 *     <to>             -> .to<toLeaves>
 *     <not>            -> .not<notLeaves>
 *     <be>             -> .be<beLeaves>
 *     <toLeaves>       -> <be> | <contain> | <include> | <not> | <only> | EQUAL | FAIL
 *     <notLeaves>      -> <be> | <contain> | <include> | <to> | only | EQUAL
 *     <beLeaves>       -> .A<and> | .AN<and> | .EQUAL<and> | .EMPTY<and> | .SAMEAS<and> |
 *                         .EQ | .LT<and> | .LTE<and> | .GT<and> | .GTE<and> |
 *                         .WITHIN<and> | .MATCH<and>
 *     <contain>        -> .contain<containLeaves>
 *     <containLeaves>  -> PROPERTY<and> | KEY<and> | KEYS<and>
 *     <include>        -> .include<includeLeaves>
 *     <includeLeaves>  -> PROPERTY<and>
 *     <only>           -> .only<contain>
 *     <and>            -> .and<branch> | //->
 *
 **/

// notes
// - withargs() will return an assertion object madeup of the function with the arguments,
// by judicious  use of <function>.apply(..) method
// - note that <to> and <not> have a potental infinite loop
// - <and> although not explicitly set up at construction time, and nor a method, is an
//   assertion object that gets constructed at the end of each logical juncture.
//   A logical juncture is whenever a hard wired method (such as a, an, lt, gt, etc)
//   is called.. s.t. we can have chains such as expect(x).to.be.a(y).and.sameAs(z);
// each non-method is an assertion object
// - method for 'a' and 'an' will be the same.


( function(module) {

	// shortcuts to save typing:
	var cl = console.log,
		cdir = console.dir;
	
	
    // the assertion language directives symbol table
    var syntaxTable = {
        to:         ['not', 'be', 'have', 'contain', 'include', 'only', 'equal', 'fail'],
        not:        ['to', 'be', 'contain', 'include', 'only', 'equal', 'fail'],
        be:         ['a', 'an', 'equal', 'equivalent', 'empty', 'sameAs', 'eq', 'lt', 'lte', 'gt', 'gte', 'within', 'boundedBy', 'match'],
//        is:         ['of', 'equal', 'not'],
//        of:         ['type'],
        contain:    ['property', 'key', 'keys', 'object', 'objects'],
        include:    ['property', 'object', 'objects'],
        only:       ['contain'],
		have:		['own'],
		whos:		['method'],
		method:		['equals']
    }


    function expect(obj) {
        return new Assertion(obj);
    }
	
	var sfy;
    
    function Assertion(obj, term, token) {
        // constructor used to build recursively the entire set of paths possible for the
        // assertion string.
        //
        // note if term (an assertion object) and token are undefined, we are called by the
        // expect() function normally, which means we are at the start of the assertion
        // in which case we build the assertion structure from the start.
        //
        // otherwise, we are building the term for the next token, using the previous term as the
        // starting point.
        //
        this.obj = obj;
        this.tokens = {}; // we keep the array of set tokens here as we build the assertion string     
        
        if (term) {
            // this term needs to observe the incoming term's tokens set
            this.tokens[token] = true;
            for (var t in term.tokens) {
                term.tokens.hasOwnProperty(t) &&
                    ( this.tokens[t] = term.tokens[t] );
            }
        }
        
        var FIRST =  token ? syntaxTable[token] : keys(syntaxTable);
		            
        if (FIRST) {
            var thisTerm = this,
                i = 0,
                subTerm,
                follow;
                
            // let's iterate through the symbols in the FIRST set
            for (i; i < FIRST.length; i++) {
                follow = FIRST[i]; 
                if (this.tokens[follow]) continue;  // recursion !!
                
				subTerm = new Assertion(this.obj, thisTerm, follow);
				
                if (typeof Assertion.prototype[follow] == 'function') {
                    // clone, otherwise prototype get's clobbered!
                    this[follow] = bindContext(this[follow], thisTerm);
                    
                    // now we need to ensure that we get the behaviours such that this[follow]
                    // can act as a pass through object, ie. we can do: expect(..).term.etc, where term here
                    // is this[follow]
                    // OR  can act as a normal method: expect(..).name(..) etc
                    //
                    // Augment the this[follow] method with the prototype methods
                    for (var f in Assertion.prototype) {
                        // change context for each method other than for FIRST[i]
                        if ( Assertion.prototype.hasOwnProperty(f) && f != follow ) {
                            this[follow][f] = bindContext(subTerm[f], subTerm);
                        }
                    }
                    continue;
                }
                this[follow] = subTerm;
            }
        }
    }
    
    
    Assertion.prototype.assert = function(truthVal, mesg, errMesg) {
        var ok = this.tokens.not ? !truthVal : truthVal,
            applyMesg = this.tokens.not ? errMesg : mesg;
			
		module.debugON && cl('assert: ', truthVal, ok, applyMesg.call(this));
		
        if (!ok) throw new AssertionError('AssertionError', applyMesg.call(this));
        
		// we've reached the end of the assertion, so return an 'and' s.t. we can
		// extend the assertion!
		
		module.debugON && cl('assert: creating .and');
		
        this.and = new Assertion(this.obj);
    }
    
    Assertion.prototype.a = Assertion.prototype.an = 
    Assertion.prototype.type = function(t) {
        // if t is a string then we are given a descriptor such as 'function', 'number' etc
        // and not an actual Constructor , such as Function, Boolean, MyClassType, etc
        if ( type(t) == 'string' ) {
            this.assert(
                type(this.obj) == t,  // assumes t is all lowercase!
                function() { return ['Expected', sfy(this.obj), 'to be a', t].join(' ') },
                function() { return ['Expected', sfy(this.obj), 'not to be a', t].join(' ') }
            );
        } else {
            // t is a Constructor
            var customType = t.name || 'Custom Type';
            this.assert(
                this.obj instanceof t,
                function() { return ['Expected', sfy(this.obj), 'to be a', customType].join(' ') },
                function() { return ['Expected', sfy(this.obj), 'not to be a', customType].join(' ') }
            );
        }
		return this;
    }
    
    Assertion.prototype.ok = function() {
        this.assert(
            !!this.obj,  // test obj's truth worthiness
            function() { return ['Expected', sfy(this.obj), 'to be truthy'].join(' ') },
            function() { return ['Expected', sfy(this.obj), 'to be falsy'].join(' ') }
        );
        return this;
    }
    
    Assertion.prototype.key = 
    Assertion.prototype.keys = function(hash) {
		// organise our hash list first
		var hash = (type(hash) == 'array') ? hash : convertToArray(arguments);
		
		if ( !hash.length ) throw new Error('ERROR: no keys given');
		
		var objKeys = Object.keys(this.obj),
			objKeysLen = objKeys.length,
			pass = isSubset(objKeys, hash);

		if (this.tokens.only) {
			if (!this.tokens.not) {
				pass = ok && objKeyLen == hash.length;
			}
		}
		
		this.assert(
			pass,
			function() { return ['Expected', sfy(this.obj), 'to', this.tokens.only ? 'only have' : 'include', sfy(hash)].join('') },
			function() { return ['Expected', sfy(this.obj), 'not to', this.tokens.only ? 'only have' : 'include', sfy(hash)].join('') }
		);
		return this;
	}
    
	Assertion.prototype.property = function(property, value) {
		// check if 'own' prop is being asked for; and if so then we only check for
		// prop 'own' existence and not bother with value check
		//
		// later **** var checkForBoth = property && value ? true : false;
		
		if (this.tokens.own) {
			this.assert(
				Object.prototype.hasOwnProperty.call(this.obj, property),
				function() { return ['Expected', sfy(this.obj), 'to have own property', property].join('') },
				function() { return ['Expected', sfy(this.obj), 'to not have own property', property].join('') }
			);
			return this;
		}
	
		// if we are here then we are checking for prop within the objects structure somewhere, eg
		// it could be buried in the prototype of the object.
		// and we also check for it's value, if we are passed a valid value.
		//
		
		// does the prop exist and is the value defined?
		//
		if (type(this.obj) != 'object') {
			throw new AssertionError( 'AssertionError', ['ERROR: ', this.obj, 'is not an object and has no property', property].join(' ') );
		}
		
		// pure property check here
		if (property in this.obj) {	// check to see property itself is defined
			this.assert(
				this.obj[property] != undefined,	// ie. property has a defined value
				function() { return ['Expected', sfy(this.obj), 'to have property instance', sfy(property)].join(' ') },
				function() { return ['Expected', sfy(this.obj), 'not to have property instance', sfy(property)].join(' ') }
			);
		} else {
			this.assert(
				false,	// ie. property itself does not exist
				function() { return ['Expected', sfy(this.obj), 'to have property', sfy(property)].join(' ') } ,
				function() { return ['Expected', sfy(this.obj), 'not to have property', sfy(property)].join('') }
			);
		
		}
		
		// now check if the property value is as expected
		if (value !== undefined) {
			this.assert(
				this.obj[property] === value,
				function() { return ['Expected', sfy(this.obj), 'to have property', property, 'with value', sfy(value)].join(' ') },
				function() { return ['Expected', sfy(this.obj), 'not to have property', property, 'with value', sfy(value)].join(' ') }
			);
		} else {
			//throw new AssertionError( 'AssertionError', ['ERROR: Checking against undefined value in', property].join(' ') );
		}
		return this;
		
	}	// function property
    
	// deep equal.. does a prop to prop comparison (ie structure + content)
	// for now, we only need deep equality to apply to own properties - fancy shmancy
	// like regular expressions equality stuff later
	//
    Assertion.prototype.equal =  
    Assertion.prototype.equals = function(obj) {
		var obj = obj;
		
		function innerEqls(first, second) {
			// let's eliminate the obvious first:
			//
			if ( type(first) == 'undefined' || type(second) == 'undefined') return false;
			if ( first === second ) return true;	// are this.obj and obj the same?
			if (type(first) != type(second) ) return false;
			if (first instanceof Array) {
				if (first.length != second.length) return false;
				for (var i = 0; i < second.length; i++) {
					if (first[i] !== second[i]) return false;
				}
				return true;
			}
			if (first instanceof Date && second instanceof Date)
				return first.getTime() === second.getTime();
			
			// test for non object type.. ie. comparing 2 numbers, bools, functions..
			if (type(first) != 'object' && type(second) != 'object') return first == second;
			
			// finally! we have 2 seperate items which happen to be be objects..
			// so let's check prop by prop!  remember we're only doing the own props for now.
			
			var firstKeys = Object.keys(first),		// Object.keys only returns own props
				secondKeys = Object.keys(second);
			
			module.debugON && cl('equal/innerEqls (keys): ', firstKeys);
			
			if (firstKeys.length != secondKeys.length) return false;
			
			// order of keys should not matter as long as keys and values match!!
			var p;
			
			firstKeys = firstKeys.sort();
			secondKeys = secondKeys.sort();
			for (var i = 0; i < firstKeys.length; i++) {
				p = firstKeys[i];
				module.debugON && cl('equal/innerEqls (test f[p] equals s[p])', p, first[p], second[p]);
				
				if ( firstKeys[i] != secondKeys[i] ) return false;  // the cheap test first
				if ( !innerEqls(first[p], second[p]) ) return false;  // the expensive test
				
			}

			return true;
		}  // innerEqls
			
		this.assert(
			innerEqls(this.obj, obj),
            function() { return ['Expected',sfy(this.obj), 'to be equal to', sfy(obj)].join(' ') },
            function() { return ['Expected',sfy(this.obj), 'to not be equal to', sfy(obj)].join(' ') }
		);
		
		module.debugON && cl('equal/innerEqls (back!)', Object.keys(this));
		
		return this;
	}
    
	// check if we have the same object
    Assertion.prototype.sameAs = 
	Assertion.prototype.be = function(oo) {
        this.assert(
            this.obj === oo,    // ie test if the 2 objects are one and the same
            function() { return ['Expected',sfy(this.obj), 'to be same as', sfy(oo)].join(' ') },
            function() { return ['Expected',sfy(this.obj), 'to not be same as', sfy(oo)].join(' ') }
        );
		return this;
    }
	
	
    // arithmetic '='
    Assertion.prototype.eq = function(value) {
        // method applicable to numbers only.
        
        this.assert(
            this.obj == value,
            function() { return ['Expected', this.obj, 'to be equal to', value].join(' ') },
            function() { return ['Expected', this.obj, 'not to be equal to', value].join(' ') }
        );
        return this;
    }
    
    Assertion.prototype.lt = function(value) {
        this.assert(
            this.obj < value,
            function() { return ['Expected', this.obj, 'to be less than', value].join(' ') },
            function() { return ['Expected', this.obj, 'to be greater than or equal to', value].join(' ') }
        );
        return this;
    }
    
    Assertion.prototype.lte = function(value) {
        this.assert(
            this.obj <= value,
            function() { return ['Expected', this.obj, 'to be less than or equal to', value].join(' ') },
            function() { return ['Expected', this.obj, 'to be greater than', value].join(' ') }
        );
        return this;
    }
    
    Assertion.prototype.gt = function(value) {
        this.assert(
            this.obj > value,
            function() { return ['Expected', this.obj, 'to be greater than', value].join(' ') },
            function() { return ['Expected', this.obj, 'to be less than or equal to', value].join(' ') }
        );
        return this;
    }

    Assertion.prototype.gte = function(value) {
        this.assert(
            this.obj > value,
            function() { return ['Expected', this.obj, 'to be greater than or equal to', value].join(' ') },
            function() { return ['Expected', this.obj, 'to be less than', value].join(' ') }
        );
        return this;
    }

    Assertion.prototype.within = function(lowerb, upperb) {
        // lower <= this.obj <= upper
        this.assert(
            lowerb <= this.obj && this.obj <= upperb ,
            function() { return ['Expected', this.obj, 'to be bounded by [', lowerb, ', ', upperb, ']'].join(' ') },
            function() { return ['Expected', this.obj, 'to not be bounded by [', lowerb, ', ', upperb, ']'].join(' ') }
        );
        return this;
    }
    
    
    Assertion.prototype.match = function() {/* TBD */}
    
	// used to test method props of objects
	Assertion.prototype.method = function(m) {
		expect(this.obj).to.have.property(m);
	}
	
    Assertion.prototype.withArgs = function() {
        expect(this.obj).to.be.type('function');  // will throw an error if not!
        var f = this.obj,
            args = Array.prototype.slice.call(arguments);
        
        // return a new expect in order to leave the original for other tests
        return expect(function() { f.apply(null, args); });
    }    
    Assertion.prototype.fail = function() {}
    Assertion.prototype.throwException = function() {}
	
	// friend methods
	
	// very simple stringify method - only looks at own properties for now..
	// no regexp stringification either
	// showHidden unused for now
	function sfy(obj, showHidden, depth) {
		// if obj is a non-object then return it; no need for further work!
		var oType = type(obj);
		if (oType == 'undefined' || oType == 'null' ) return oType;
		if (oType == 'string' || oType == 'boolean' || oType == 'number' ) return obj.toString();
		
		var stack = [],
			depth = depth || 2,
			arType = oType == 'array' ? true : false;
			
		function internalRecurse(obj, rDepth) {
			var keys = Object.keys(obj).sort(), len = keys.length,
				openBrace = '[',  closeBrace =  ']',		// default enclosing braces
				prop, sep = [],
				val;
				
			if ( type(obj) == 'object' ) {
				 openBrace = '{';
				 closeBrace = '}';
			}
			
			for (var i = 0; i < len-1; i++) {
				sep[i] = ', ';
			}
			sep[i] = '';

			stack.push(openBrace);
			for (var i = 0; i < len; i++) {
				prop = keys[i];
                if (!arType) stack.push(prop,':');
				
				if ( type(obj[prop]) == 'object' || type(obj[prop]) == 'array' ) {
					arType = type(obj[prop]) == 'array' ? true : false;
					val = '';
					if (rDepth < depth) internalRecurse(obj[prop], rDepth+1)
					else val = type(obj[prop]);
					
				} else if (type(obj[prop]) == 'function' ) {
					val = 'function';
					
				} else {
					val = obj[prop];
				}
				stack.push(val, sep[i]);
				
			}  // for
									
			stack.push(closeBrace);
			
		}  // internalRecurse
		
		internalRecurse(obj, 0);
		return stack.join('');
	
	}

    function AssertionError(name, message) {
        this.name = name;
        this.message = message;
    }
    AssertionError.prototype = new Error();
    AssertionError.prototype.constructor = AssertionError;
        
    module.expect = expect;
    expect.Assertion = Assertion;
	expect.stringify = sfy;
	expect.AssertionError = AssertionError;
    

} )(testFrame);


